package owg.util.editor2d.modes;

import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.KeyEvent;
import java.text.DecimalFormat;

import owg.util.Calc;
import owg.util.editor2d.Editor2D;
import owg.util.editor2d.Editor2DUI;
import owg.util.editor2d.entity.Placeable2D;
import owg.util.editor2d.entity.Rotateable2D;
import owg.util.editor2d.operations.OperationFailedException;
import owg.util.editor2d.operations.Transformation;
import owg.util.euclidian.V2D;

public abstract class TransformationMode extends Mode {
	/**Formatter for three-decimal floating point output*/
	public final static DecimalFormat threeDecimal = new DecimalFormat("0.000");
	
	/**The pivot object when starting the transformation.*/
	protected final Placeable2D pivot;
	/**The pivot mote when starting the transformation.*/
	protected final Editor2D.Pivot pivotMode;
	/**Which indices in the editor's world objects list will be affected.*/
	protected final Integer[] affectedIndices;
	/**The inverse operation. Will be invoked upon an undo, or upon cancelling the operation.*/
	protected final Transformation inverse;
	
	/**The current number input as a string, excluding the minus symbol, if any. Will be null if no input is given.*/
	private String numberInput = null;
	/**Whether the number input is negative(the user has pressed the minus key an odd number of times).*/
	private boolean numberNegative = false;
	/**The current number input as a Double, including sign. Will be null if no input is given.*/
	protected Double numberValue = null; 
	/**
	 * @param e The Editor2D associated with this instance.
	 * @param initialMessage The initial message which will be posted to the editor UI. 
	 * The message may be {@link Editor2DUI#replaceMessage(String)}d by the implementation while transformation is happening.
	 * If the operation is cancelled, it will be automatically replaced with an appropriate message.
	 */
	public TransformationMode(Editor2D e, String initialMessage)
	{
		super(e);
		e.editorUI().message(initialMessage);
		affectedIndices = new Integer[e.selectedIndices.size()];
		e.selectedIndices.toArray(affectedIndices);
		inverse = new Transformation(e, affectedIndices);
		
		pivotMode = editor.settings.pivot.get();
		pivot = editor.getPivotObject();
	}

	/**
	 * @param obj The object of which we want to find the local Y axis.
	 * @return obj's local Y axis.
	 */
	protected V2D yOf(Placeable2D obj)
	{
		if(obj instanceof Rotateable2D)
			return new V2D(Math.cos(((Rotateable2D) obj).getAngle()-Math.PI/2), -Math.sin(((Rotateable2D) obj).getAngle()-Math.PI/2));
		else
			return V2D.Y;
	}
	/**
	 * @param obj The object of which we want to find the local X axis.
	 * @return obj's local X axis.
	 */
	protected V2D xOf(Placeable2D obj)
	{
		if(obj instanceof Rotateable2D)
			return new V2D(Math.cos(((Rotateable2D) obj).getAngle()), -Math.sin(((Rotateable2D) obj).getAngle()));
		else
			return V2D.X;
	}
	@Override
	public final void terminate(boolean cancel) {
		if(cancel)
		{
			editor.editorUI().replaceMessage("Transformation Cancelled.");
			try
			{
				inverse.apply(editor);
			}
			catch (OperationFailedException e)
			{
				System.err.println("Error: Could not cancel transformation: ");
				e.printStackTrace();
			}
		}
		else
		{
			Transformation op = new Transformation(editor, affectedIndices);
			editor.execute(op, inverse, true, getMessage(), true, false);
		}
		
		editor.modeFinished();
	}
	
	@Override
	public final void key(KeyEvent e)
	{	
		boolean ch = false;
		if(e.getID() == KeyEvent.KEY_PRESSED && numberInputAllowed())
		{
			int code = e.getKeyCode();
			//Allow numerical input
			//Copypasta?
			if(code == KeyEvent.VK_V && e.getModifiers() == KeyEvent.CTRL_MASK)
			{
				try
				{
					Object data = Toolkit.getDefaultToolkit()
						.getSystemClipboard().getData(DataFlavor.stringFlavor);
					Double.parseDouble((String)data); //This is to throw exception if unparsable
					numberInput = (String)data;
					ch = true;
				} 
				catch(Exception err)
				{
					System.out.println(err.getMessage());
				}
			}
			if(code == KeyEvent.VK_MINUS)
			{
				numberNegative = !numberNegative;
				ch = true;
			}
			
			if(code == KeyEvent.VK_PERIOD)
			{
				if (numberInput == null)
				{
					numberInput = "0.";
					ch = true;
				}
				else if(!numberInput.contains("."))
				{
					numberInput += '.';
					ch = true;
				}
			}
			
			if(code == KeyEvent.VK_BACK_SPACE && numberInput != null)
			{
				if (numberInput.length()>1)
					numberInput = numberInput.substring(0, numberInput.length()-1);
				else
				{
					numberInput = null;
					numberNegative = false;
				}
				ch = true;
			}
			
			for(int i = 0; i<10; i++)
			{
				int virtualKey = KeyEvent.VK_0+i;
				if(code == virtualKey)
				{
					char c = (char)((int)'0'+i);
					if(numberInput == null)
						numberInput = String.valueOf(c);
					else
						numberInput += c;
					ch = true;
				}
			}
		}
		if(ch)
		{
			numberValue = getNumberInputValue();
			numberInputChanged(e);
		}
		else
			key2(e);
	}
	/**Clear the current number input from the keyboard. In general, 
	 * the implementation should call this if the condidion for {@link #numberInputAllowed()} changes from true to false.*/
	protected void clearNumberInput()
	{
		numberInput = null;
		numberNegative = false;
		numberValue = null;
	}

	public void setNumberValue(double value)
	{
		if(value < 0)
		{
			numberInput = String.valueOf(Math.abs(value));
			numberNegative = true;
		}
		else 
		{
			numberInput = String.valueOf(value);
			numberNegative = false;
		}
		numberValue = value;
		numberInputChanged(null);
	}
	/**@return Whether the mode currently allows number input via the keyboard.*/
	protected abstract boolean numberInputAllowed();
	/**
	 * Called when the number input {@link #numberValue} changes as a result of a keyboard event. Note that the value may have become null.
	 * @param e The key event which caused the change in the number input.
	 */
	protected abstract void numberInputChanged(KeyEvent e);
	/**Called when a key event is received which did not cause a change in {@link #numberValue}. 
	 * Note that all key events will be sent here if {@link #numberInputAllowed()} is false.*/
	protected abstract void key2(KeyEvent e);
	
	/**Convert the current numerical input to a number, null if none.*/
	private Double getNumberInputValue()
	{
		if(numberInput != null)
		{
			try
			{
				return (numberNegative?-1:1)*Double.parseDouble(numberInput);
			}
			catch(NumberFormatException e)
			{
				clearNumberInput();
				System.err.println("Unexpected error in parsing "+numberInput);
				e.printStackTrace();
				return null;
			}
		}
		else
			return null;
	}
	private String getNumberInputString()
	{
		if(numberInput == null)
			return null;
		else if(numberNegative)
			return "-"+numberInput;
		else
			return numberInput;
	}
	protected String numericalValueString(double literalvalue)
	{
		final String numberInput = getNumberInputString(); 
		if(this instanceof Rotate)
		{
			final String degrees;
			if(numberInput == null)
				degrees = threeDecimal.format(literalvalue*Calc.toDeg);
			else
				degrees = numberInput;
			return degrees+"`("+literalvalue+")";
		}
		else if (numberInput==null)
			return String.valueOf(literalvalue);
		else
			return numberInput;
	}
	/**Called when the operation is being completed.*/
	protected abstract String getMessage();
	

	@Override
	public boolean shouldUpdateHighlight()
	{
		return true;
	}
}
